package util.file.xml;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import util.file.CodificacionTexto;
import util.patron.command.Command1;

public class XmlUtil { 
    

    
    //http://stackoverflow.com/questions/11473805/create-a-xml-file-using-jsp-coding-and-store-in-local-disk-folder
    public static void agregarNodoTexto(Document doc, Element nodoPadre, String nombreNodo, String valorNodo) {
        Element e = doc.createElement(nombreNodo);
        String data = (valorNodo==null? null : valorNodo.trim());
        if(data!=null)
            e.appendChild(doc.createTextNode(data));
        nodoPadre.appendChild(e);
    }
    
              
    //XML Document to String
    //http://stackoverflow.com/questions/5456680/xml-document-to-string
    //http://stackoverflow.com/questions/11473805/create-a-xml-file-using-jsp-coding-and-store-in-local-disk-folder
    public static String documentXmlToString(
            Document doc, 
            boolean eliminarEscapes, 
            boolean omitirDeclaracionXml,
            CodificacionTexto codificacion,
            boolean indentar) throws TransformerConfigurationException, TransformerException  {
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        if(omitirDeclaracionXml) {
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        }
        else {
            if(codificacion!=null) {
                //http://stackoverflow.com/questions/13514996/split-xml-file-with-the-new-files-with-encoding-iso-8859-1
                transformer.setOutputProperty(OutputKeys.ENCODING,codificacion.getNombre());
            }
        }
        
        if(indentar)
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter sw = new StringWriter();
        transformer.transform(new DOMSource(doc), new StreamResult(sw));
        
        //Eliminando caracteres de escape
        return eliminarEscapes ? sw.getBuffer().toString().replaceAll("\n|\r", "") : sw.toString();
    }
    
    public static String documentXmlToStringCompacto(Document doc, CodificacionTexto codificacion) 
            throws TransformerConfigurationException, TransformerException  {
        return documentXmlToString(doc,true,true,codificacion,false);
    }
    
    public static String documentXmlToString(Document doc, CodificacionTexto codificacion) 
            throws TransformerConfigurationException, TransformerException  {
        return documentXmlToString(doc,false,false,codificacion,true);
    }
    
    //http://stackoverflow.com/questions/9578815/does-documentbuilder-parse-close-the-inputstream
    public static void verSiInputStreamEstaCerrado(String urlXml) {
        InputStream is = null;
        try {
            is = new MyInputStream(urlXml);
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
            documentBuilder.parse(is);
        } catch (Exception e) {
            //imprime la traza de la excepcion en pantalla. 
            //Alli se puede hacer seguimiento para ver quién abrio el archivo
            e.printStackTrace();
            
            if(is!=null) try {
                is.close();
            } catch (IOException ex) {
                //Logger.getLogger(XmlUtil.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
    
    static class MyInputStream extends FileInputStream {   
        public MyInputStream(String filename) throws FileNotFoundException {
          super(filename);
        }

        @Override
        public void close() throws IOException {
            // here we log when the stream is close.
            System.out.println("file input stream closed.");
            Exception e = new Exception();
            e.printStackTrace();
            super.close();
        }
    }
    
    /////////////////////////////////////////////////////////////////////////////////////
    
    //Los hijos se pueden repetir    
    public static List<Node> getNodosHijoByNombreTag(Document docXml, String tag, boolean incluirNodosTexto) throws Exception {
        NodeList lnTags = docXml.getElementsByTagName(tag);//lista de nodos Tag
        Node nTag = lnTags.item(0);//Primer nodo Tag       
        return getNodosHijo(nTag,incluirNodosTexto);
    }
    
    public static List<Node> getNodosHijo(Node nodo, boolean incluirNodosTexto) throws Exception {        
        final List<Node> nodosHijo = new ArrayList<Node>();        
        Command1<Node> op = new Command1<Node>() {                    
            @Override
            public void execute(Node hTag) {
                nodosHijo.add(hTag);
            }
        };        
        operarNodosHijo(nodo,incluirNodosTexto,op);//inyectando código       
        return nodosHijo;
    }
    
    public static Map<String,Node> getNodosHijoUnicosByNombreTag(Document docXml, String tag) throws Exception {        
        return getNodosHijoUnicosByNombreTag(docXml,tag,false);
    }
    
    public static Map<String,Node> getNodosHijoUnicosByNombreTag(Document docXml, String tag, boolean incluirNodosTexto) throws Exception {        
        NodeList lnTags = docXml.getElementsByTagName(tag);//lista de nodos Tag
        Node nTag = lnTags.item(0);//Primer nodo Tag  
        if(nTag==null)
            throw new Exception("No se encontró el nodo '"+tag+"' en el archivo. Posiblemente no es el archivo correcto.");
        return getNodosHijoUnicos(nTag,incluirNodosTexto);
    }
    
    //Suposicion: los nodos hijo de "tag" no se repiten
    public static Map<String,Node> getNodosHijoUnicos(Node nodo, boolean incluirNodosTexto) throws Exception {        
        
        final Map<String,Node> nodosHijo = new HashMap<String,Node>();
        
        Command1<Node> op = new Command1<Node>() {                    
            @Override
            public void execute(Node hTag) {
                nodosHijo.put(hTag.getNodeName(), hTag);
            }
        };
        
        operarNodosHijo(nodo,incluirNodosTexto,op);//inyectando código    
        return nodosHijo;
    }
    

    /*
    private static void operarNodosHijoByNombreTag(Object obj, Operacion op, Document docXml, String tag, boolean incluirNodosTexto) {
        //ln: lista de nodos
                
        NodeList lnTags = docXml.getElementsByTagName(tag);//lista de nodos Tag
        Node nTag = lnTags.item(0);//Primer nodo Tag
        operarNodosHijo(obj,op,nTag,incluirNodosTexto);
    }
    */
    
    private static void operarNodosHijo(Node nTag, boolean incluirNodosTexto, Command1<Node> operador) throws Exception {
        //hs: hijos
        //h: hijo
        //n: nodo     
                
        NodeList hsTag = nTag.getChildNodes();//Hijos del nodo Tag
        
        int nhsTag = hsTag.getLength();//numero de hijos de Tag
        
        if(incluirNodosTexto) {
            for (int i = 0; i < nhsTag; i++) {
                Node hTag = hsTag.item(i);                       

                //--------------------------------------------
                //Inyectando código
                //--------------------------------------------
                operador.execute(hTag);
            }
        } else {
            for (int i = 0; i < nhsTag; i++) {
                Node hTag = hsTag.item(i);

                //descartando los nodos de tipo texto                                
                if(hTag.getNodeType()==Node.TEXT_NODE)//if(nTabla.getNodeName().equals("#text"))
                    continue;           

                //--------------------------------------------
                //Inyectando código
                //--------------------------------------------
                operador.execute(hTag);            
            }
        }
    }
    
    //http://stackoverflow.com/questions/15732/whats-the-best-way-to-validate-an-xml-file-against-an-xsd-file
    public void validarArchivoXMLContraXSD(String archivoXml, String archivoXSD) throws MalformedURLException, SAXException, IOException {
        //Ejemplo:        
        //URL schemaFile = new URL("http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd");
        //Source xmlFile = new StreamSource(new File("web.xml"));
        
        URL schemaFile = new URL(archivoXSD);
        Source xmlFile = new StreamSource(new File(archivoXml));        
        SchemaFactory schemaFactory = SchemaFactory
            .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Schema schema = schemaFactory.newSchema(schemaFile);
        Validator validator = schema.newValidator();
        try {            
          validator.validate(xmlFile);
          System.out.println(xmlFile.getSystemId() + " is valid");
        } catch (SAXException e) {
          System.out.println(xmlFile.getSystemId() + " is NOT valid");
          System.out.println("Reason: " + e.getLocalizedMessage());
        }
    }
}
